package net.cathackers.devel.scmp.chat.impl.xmpp;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.KeyStore;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;

import org.apache.log4j.Logger;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.filterchain.IoFilterChain;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.compression.CompressionFilter;
import org.apache.mina.filter.ssl.SslFilter;
import org.dom4j.Document;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;

import net.cathackers.devel.scmp.chat.impl.TLSConfig;
import net.cathackers.devel.scmp.chat.impl.xmpp.messageComponent.MessageComponent;
import net.cathackers.devel.scmp.chat.interfaces.XMPPConnection;
import net.cathackers.devel.scmp.config.impl.SCMPConstants;
import net.cathackers.devel.scmp.exceptions.NotAllowedException;
import net.cathackers.devel.scmp.factories.SCMPFactory;
import net.cathackers.devel.scmp.xmpp.MessagePacket;
import net.cathackers.devel.scmp.xmpp.Packet;

/**
 * The role of this class to manage the state of the connection and handles the
 * outgoing messages.
 * 
 * @author Ahmed Soliman
 */
public class XMPPConnectionImpl implements XMPPConnection {
    private IoSession ioSession_;
    private String sessionID_;
    private Logger logger_;
    private long lastActive_ = 0;
    private boolean flashClient_ = false;
    private int majorVersion_ = 1;
    private int minorVersion_ = 0;
    private String language_ = null;
    private boolean closed_ = false;
    private TLSPolicy tlsPolicy_;
    private CompressionPolicy compressionPolicy_ = CompressionPolicy.disabled;
    private MessageComponent msgComponent_;

    public XMPPConnectionImpl(IoSession session) {
	ioSession_ = session;
	logger_ = SCMPFactory.getLogger(this);
	tlsPolicy_ = SCMPFactory.getXMPPConfiguration().getTLSPolicy();
	msgComponent_ = new MessageComponent(SCMPFactory.getOfflineMessageStore());
    }

    @Override
    public void addCompression() {
	IoFilterChain chain = ioSession_.getFilterChain();
	String baseFilter = "pooling";
	if (chain.contains("tls")) {
	    baseFilter = "tls";
	}
	// if we have tls encryption, we have to add the compression after this
	// layer, logical, huh?
	chain.addAfter(baseFilter, "compression", new CompressionFilter(true,
		false, CompressionFilter.COMPRESSION_MAX));

    }

    @Override
    public void startCompression() {
	CompressionFilter ioFilter = (CompressionFilter) ioSession_
		.getFilterChain().get("compression");
	// start compressing the outgoing data.
	ioFilter.setCompressOutbound(true);
    }

    @Override
    public synchronized void close() {
	if (!isClosed()) {
	    logger_.info("Connection " + getSessionID() + " bye()");
	    try {
		deliverString(flashClient_ ? "</flash:stream>"
			: "</stream:stream>", false);
	    } catch (Exception e) {
		// Safely Ignore
		logger_.error("un Expected error" + e.getMessage());
	    }
	    logger_.info("Connection " + getSessionID() + " closed");
	    ioSession_.close(false);
	    closed_ = true;
	}
    }

    @Override
    public synchronized void deliver(Packet packet) throws NotAllowedException {
	deliver(packet.getDocument());
    }

    @Override
    public synchronized void deliver(Document packet)
	    throws NotAllowedException {
	/*
	 * We don't want to do that because the xml generated has too many
	 * spaces so for optimization we are going to use the XMLWriter here
	 */
	logger_.debug("Will send:"  + packet.asXML());
	IoBuffer buffer = IoBuffer.allocate(2048);
	buffer.setAutoExpand(true);
	OutputStream out = buffer.asOutputStream();
	try {
	    OutputFormat format = new OutputFormat();
	    format.setNewLineAfterDeclaration(false);
	    format.setSuppressDeclaration(true);
	    XMLWriter myWriter = new XMLWriter(out, format);
	    myWriter.write(packet);
	    myWriter.flush();
	    logger_.debug("convert packet to xml for delivering");
	    if (flashClient_) {
		buffer.put((byte) '\0');
	    }
	    buffer.flip();
	    ioSession_.write(buffer);
	    // we couldn't deliver the message
	    // so we will store that message to the offline messages store
	} catch (UnsupportedEncodingException e) {
	    logger_.error("Error delivering packet to the user("
		    + this.toString() + "):" + e.getMessage());
	    if (packet instanceof MessagePacket) {
		 msgComponent_.deliverToBackup(packet);	
	    }
	   
	} catch (IOException e) {
	    logger_.error("Error delivering packet to the user("
		    + this.toString() + "):" + e.getMessage());
	    if (packet instanceof MessagePacket) {
		msgComponent_.deliverToBackup(packet);
	    }
	}

    }

    @Override
    public synchronized void deliverString(String text) {
	// normally we want async. communication
	logger_.debug("Will send:"  + text);
	deliverString(text, true);
    }

    public synchronized void deliverString(String text, boolean async) {
	// we had to disable the encoder because we need to manipulate
	// buffer for flash clients and this is the place where we know
	// whether the client is flash or not.
	if (!isClosed()) {
	    IoBuffer buffer = IoBuffer.allocate(text.length());
	    buffer.setAutoExpand(true);

	    boolean errorDelivering = false;
	    try {
		buffer.put(text.getBytes(SCMPConstants._ENCODING_));
		if (flashClient_) {
		    buffer.put((byte) '\0');
		}
		buffer.flip();
		if (async) {
		    ioSession_.write(buffer);
		} else {
		    // Send stanza and wait for ACK (using a 2 seconds default
		    // timeout)
		    logger_.debug("Send stanza and wait for ACK");
		    boolean ok = ioSession_.write(buffer).awaitUninterruptibly(
			    2000);
		    if (!ok) {
			logger_
				.warn("No ACK was received when sending stanza to: "
					+ ioSession_.toString());
		    }
		}
	    } catch (Exception ex) {
		logger_.debug("Error delivering raw text" + "\n"
			+ ioSession_.toString(), ex);
		errorDelivering = true;
	    }
	    // Close the connection if delivering text fails and we are already
	    // not closing the connection
	    if (errorDelivering) {
		close();
	    }
	}

    }

    @Override
    public CompressionPolicy getCompressionPolicy() {
	return compressionPolicy_;
    }

    @Override
    public String getLanguage() {
	return language_;
    }

    @Override
    public int getMajorXMPPVersion() {
	return majorVersion_;
    }

    @Override
    public int getMinorXMPPVersion() {
	return minorVersion_;
    }

    @Override
    public TLSPolicy getTLSPolicy() {
	return tlsPolicy_;
    }

    @Override
    public boolean heartBeat() {
	if (isClosed()) {
	    return false;
	}
	deliverString(" ");
	return !isClosed();
    }

    @Override
    public boolean isCompressed() {
	return ioSession_.getFilterChain().contains("compression");
    }

    @Override
    public void setCompressionPolicy(CompressionPolicy compressionPolicy) {
	compressionPolicy_ = compressionPolicy;
    }

    @Override
    public void setLanaguage(String language) {
	language_ = language;
    }

    @Override
    public void setTLSPolicy(TLSPolicy tlsPolicy) {
	tlsPolicy_ = tlsPolicy;
    }

    @Override
    public void setXMPPVersion(int majorVersion, int minorVersion) {
	majorVersion_ = majorVersion;
	minorVersion_ = minorVersion;

    }

    @Override
    public void startTLS(boolean clientMode, String remoteServer,
	    ClientAuth authentication) throws Exception {
	logger_.debug("XMPPConnection: startTLS");
	 KeyStore ksKeys = TLSConfig.getKeyStore();
	 String keypass = TLSConfig.getKeyPassword();
	//
	 KeyStore ksTrust = (TLSConfig.getc2sTrustStore());
	 String trustpass = (TLSConfig.getc2sTrustPassword());
	 
	        // KeyManager's decide which key material to use.
	        KeyManager[] km = TLSConfig.getKeyManagers(ksKeys, keypass);

	        // TrustManager's decide whether to allow connections.
	        TrustManager[] tm = TLSConfig.getTrustManagers(ksTrust, trustpass);

	        if (clientMode || authentication == ClientAuth.needed || authentication == ClientAuth.wanted) {
	            logger_.error("Currently we only support Disabled TLS Client Authentication");
	            // We might need to verify a certificate from our peer, so get different TrustManager[]'s
	                // Check if we can trust certificates presented by the client
	        }

	        SSLContext tlsContext = SSLContext.getInstance("TLS");

		    tlsContext.init(km, tm, null);

	        SslFilter filter = new SslFilter(tlsContext);
	        filter.setUseClientMode(clientMode);
	        if (authentication == ClientAuth.needed) {
	            filter.setNeedClientAuth(true);
	        }
	        else if (authentication == ClientAuth.wanted) {
	            // Just indicate that we would like to authenticate the client but if client
	            // certificates are self-signed or have no certificate chain then we are still
	            // good
	            filter.setWantClientAuth(true);
	        }
	        ioSession_.getFilterChain().addBefore("pooling", "tls", filter);
	        ioSession_.setAttribute(SslFilter.DISABLE_ENCRYPTION_ONCE, Boolean.TRUE);
	        if (!clientMode) {
	            // Indicate the client that the server is ready to negotiate TLS
	            deliverString("<proceed xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
	        }
    }

    @Override
    public void systemShutdown() {
	deliverString("<stream:error><system-shutdown "
		+ "xmlns='urn:ietf:params:xml:ns:xmpp-streams'/></stream:error>");
	close();
    }

    @Override
    public boolean isClosed() {
	boolean ret = closed_;
	return ret;
    }

    @Override
    public boolean isFlashClient() {
	return flashClient_;
    }

    @Override
    public void setFlashClient(boolean flashClient) {
	flashClient_ = flashClient;

    }

    @Override
    public boolean isTLS() {
	return ioSession_.getFilterChain().contains("tls");
    }

    @Override
    public long getLastActive() {
	return lastActive_;
    }

    @Override
    public void setLastActive(long timestamp) {
	lastActive_ = timestamp;

    }

    @Override
    public String toString() {
	return ioSession_.toString();
    }

    @Override
    public String getSessionID() {
	return sessionID_;
    }

    @Override
    public void setSessionID(String sessionID) {
	sessionID_ = sessionID;
    }

}
